package org.type4neo;

import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.kernel.Config;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.kernel.impl.transaction.TransactionFailureException;
import org.type4neo.bytecode.DynamicTyper;
import org.type4neo.imp.NodeWrapperIterator;

public class TypedEmbeddedGraphDatabase implements TypedGraphDatabaseService {
	private final EmbeddedGraphDatabase neoImpl;

	/**
	 * Creates an embedded {@link TypedGraphDatabaseService} with a store located in
	 * <code>storeDir</code>, which will be created if it doesn't already exist.
	 * 
	 * @param storeDir
	 *            the store directory for the Neo4j store files
	 */
	public TypedEmbeddedGraphDatabase(String storeDir) {
		this.neoImpl = new EmbeddedGraphDatabase(storeDir);
	}

	/**
	 * A non-standard way of creating an embedded {@link TypedGraphDatabaseService}
	 * with a set of configuration parameters. Will most likely be removed in
	 * future releases.
	 * <p>
	 * Creates an embedded {@link TypedGraphDatabaseService} with a store located in
	 * <code>storeDir</code>, which will be created if it doesn't already exist.
	 * 
	 * @param storeDir
	 *            the store directory for the db files
	 * @param params
	 *            configuration parameters
	 */
	public TypedEmbeddedGraphDatabase(String storeDir, Map<String, String> params) {
		this.neoImpl = new EmbeddedGraphDatabase(storeDir, params);
	}

	/**
	 * A non-standard convenience method that loads a standard property file and
	 * converts it into a generic <Code>Map<String,String></CODE>. Will most
	 * likely be removed in future releases.
	 * 
	 * @param file
	 *            the property file to load
	 * @return a map containing the properties from the file
	 * @throws IllegalArgumentException
	 *             if file does not exist
	 */
	public static Map<String, String> loadConfigurations(String file) {
		return EmbeddedGraphDatabase.loadConfigurations(file);
	}

	/**
	 * This method is deprecated in favor of {@link #createNodeAs}. If you use it it will throw an UnsupportedOperationException
	 * 
	 * @throws UnsupportedOperationException If called.
	 * @deprecated Use {@link #createNodeAs}
	 */
	@Deprecated
	public Node createNode() {
		throw new UnsupportedOperationException("Creating basic nodes is not supported on a TypedEmbeddedGraphDatabase");
	}

	/**
     * Looks up a node by id.
     * 
     * @param id the id of the node
     * @return the node with id <code>id</code> if found
     * @throws NotFoundException if not found
     * @see TypedGraphDatabaseService#getNodeById
	 */
	public Node getNodeById(long id) {
		return DynamicTyper.wrapNode(neoImpl.getNodeById(id));
	}

	/**
     * Looks up a relationship by id.
     * 
     * @param id the id of the relationship
     * @return the relationship with id <code>id</code> if found
     * @throws NotFoundException if not found
     */
	public Relationship getRelationshipById(long id) {
		return DynamicTyper.wrapRelationship(neoImpl.getRelationshipById(id));
	}

	/**
     * Returns the reference node, which is a "starting point" in the node
     * space. Usually, a client attaches relationships to this node that leads
     * into various parts of the node space. For more information about common
     * node space organizational patterns, see the design guide at <a
     * href="http://wiki.neo4j.org/content/Design_Guide"
     * >wiki.neo4j.org/content/Design_Guide</a>.
     * 
     * @return the reference node
     * @throws NotFoundException if unable to get the reference node
     */
	public Node getReferenceNode() {
		return DynamicTyper.wrapNode(neoImpl.getReferenceNode());
	}

	/**
     * Shuts down Neo4j. After this method has been invoked, it's invalid to
     * invoke any methods in the Neo4j API and all references to this instance
     * of GraphDatabaseService should be discarded.
     */
	public void shutdown() {
		neoImpl.shutdown();
	}

	/**
     * Enables remote shell access (with default configuration) to this Neo4j
     * instance, if the Neo4j <a
     * href="http://components.neo4j.org/neo4j-shell/">shell component</a> is
     * available on the classpath. This method is identical to invoking
     * {@link #enableRemoteShell(Map) enableRemoteShell( null )}.
     * 
     * @return <code>true</code> if the shell has been enabled,
     *         <code>false</code> otherwise (<code>false</code> usually
     *         indicates that the <code>shell</code> jar dependency is not on
     *         the classpath)
     */
	public boolean enableRemoteShell() {
		return neoImpl.enableRemoteShell();
	}

	/**
     * Enables remote shell access to this Neo4j instance, if the Neo4j <a
     * href="http://components.neo4j.org/neo4j-shell/">Shell component</a> is
     * available on the classpath. This will publish a shell access interface on
     * an RMI registry on localhost (with configurable port and RMI binding
     * name). It can be accessed by a client that implements
     * org.neo4j.shell.ShellClient from the Neo4j Shell project.
     * Typically, the <code>neo4j-shell</code> binary package is used (see <a
     * href="http://neo4j.org/download">neo4j.org/download</a>).
     * <p>
     * The shell is parameterized by a map of properties passed in to this
     * method. Currently, two properties are used:
     * <ul>
     * <li><code>port</code>, an {@link Integer} describing the port of the RMI
     * registry where the Neo4j shell will be bound, defaults to
     * <code>1337</code>
     * <li><code>name</code>, the {@link String} under which the Neo4j shell
     * will be bound in the RMI registry, defaults to <code>shell</code>
     * </ul>
     * 
     * @param initialProperties a set of properties that will be used to
     *            configure the remote shell, or <code>null</code> if the
     *            default properties should be used
     * @return <code>true</code> if the shell has been enabled,
     *         <code>false</code> otherwise (<code>false</code> usually
     *         indicates that the <code>shell</code> jar dependency is not on
     *         the classpath)
     * @throws ClassCastException if the shell library is available, but one (or
     *             more) of the configuration properties have an unexpected type
     * @throws IllegalStateException if the shell library is available, but the
     *             remote shell can't be enabled anyway
     */
	public boolean enableRemoteShell(
			final Map<String, Serializable> initialProperties) {
		return neoImpl.enableRemoteShell(initialProperties);
	}

	/**
     * Returns all relationship types currently in the underlying store.
     * Relationship types are added to the underlying store the first time they
     * are used in a successfully commited {@link TypedNode#createRelationshipTo
     * node.createRelationshipTo(...)}. Note that this method is guaranteed to
     * return all known relationship types, but it does not guarantee that it
     * won't return <i>more</i> than that (e.g. it can return "historic"
     * relationship types that no longer have any relationships in the node
     * space).
     * 
     * @return all relationship types in the underlying store
     */
	public Iterable<RelationshipType> getRelationshipTypes() {
		return neoImpl.getRelationshipTypes();
	}

    /**
     * @throws TransactionFailureException if unable to start transaction
     */
	public Transaction beginTx() {
		return neoImpl.beginTx();
	}

	/**
	 * Returns a non-standard configuration object. Will most likely be removed
	 * in future releases.
	 * 
	 * @return a configuration object
	 */
	public Config getConfig() {
		return neoImpl.getConfig();
	}

	@Override
	public String toString() {
		return super.toString() + " [" + neoImpl.getStoreDir() + "]";
	}

	public String getStoreDir() {
		return neoImpl.getStoreDir();
	}

	public Iterable<Node> getAllNodes() {
		return new Iterable<Node>() {
			
			public Iterator<Node> iterator() {
				return new NodeWrapperIterator(neoImpl.getAllNodes().iterator());
			}
		};
	}

	/**
	 * Creates a new TypedNode that is assignment compatible with <code>interfaceClass</code>.
	 * 
	 * @param interfaceClass The interface this node must implement
	 * @return The new node
	 * @throws NullPointerException if the <code>interfaceClass</code> is null
	 * @throws IllegalArgumentException if <code>interfaceClass</code> is not an interface
	 */
	public TypedNode createNodeAs(Class<? extends TypedNode> interfaceClass) {
		if (interfaceClass == null) throw new NullPointerException("Interface class can't be null");
		if (!interfaceClass.isInterface()) throw new IllegalArgumentException("Given class must be an Interface");
		
		Node node = this.neoImpl.createNode();
		node.setProperty(DynamicTyper.PROTOTYPE_PROPERTY_NAME, interfaceClass.getName());
		return DynamicTyper.wrapNode(node);
	}
}